﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml;
using System.Collections;
using MethodWorx.Core.DomainModel;

namespace MethodWorx.Core.Serialization
{
    public class AggregateXmlSerializer
    {
        public List<Action<object, object>> actions = new List<Action<object, object>>();
        public void AddRelationshipResolver<TParent, TChild>(Action<TParent, TChild> func)
        {
            actions.Add((p, c) => func((TParent)p, (TChild)c));
        }

        private void InvokeRelationshipResolver(object tParent, object tChild)
        {
            foreach (Action<object, object> action in actions)
            {
                try
                {
                    action.Invoke(tParent, tChild);
                }
                catch
                {
                }
            }

        }


        public IEnumerable<Entity> Deserialize(Type [] types, IRepositoryRegistry repositories, XmlDocument document)
        {
            List<Entity> deserialized = new List<Entity>();
            foreach (var node in document.SelectNodes("AggregateRoot"))
            {
                DeserializeAggregateRoot(deserialized, types, repositories, node as XmlNode);
            }

            return deserialized;
        }

        private Entity DeserializeAggregateRoot(List<Entity> deserialized, Type [] types, IRepositoryRegistry repositories, XmlNode node)
        {
            string typeName = node.Attributes["Type"].Value;
            var type = types.FirstOrDefault(t => t.FullName == typeName);
            object instance = repositories.GetRepository(type).CreateInstance(type);

            //  add the deserialized entity here
            deserialized.Add((Entity)instance);

            DeserializeEntity(deserialized, types, repositories, node, (AggregateRoot)instance, (Entity)instance);


            return (Entity)instance;
        }

        private void DeserializeEntity(List<Entity> deserialized, Type [] types, IRepositoryRegistry repositories, XmlNode node, AggregateRoot root, Entity instance)
        {

            //
            //  first of all do all primitive properties
            this.DeserializePrimitiveProperties(node, instance);
            //
            //  now deserialize all the references
            this.DeserializeReferenceProperties(deserialized, types, repositories, node, root, instance);
            //
            //  now deserialize all the serialized properties
            this.DeserializeSerializedProperties(node, instance);
            //
            //  now deserialize all the child entity collections
            this.DeserializeChildEntityCollections(deserialized, types, repositories, node, root, instance);
        }

        private void DeserializeChildEntityCollections(List<Entity> deserialized, Type[] types, IRepositoryRegistry repositories, XmlNode node, AggregateRoot aggregateRoot, Entity parent)
        {
            foreach (XmlNode primitive in node.SelectNodes("Property[@Type='ChildEntityCollection']"))
            {
                string name = primitive.Attributes["Name"].Value;

                foreach (XmlNode entity in primitive.SelectNodes("Entity"))
                {
                    string typeName = entity.Attributes["Type"].Value;
                    var type = types.FirstOrDefault(t => t.FullName == typeName);
                    object instance = repositories.GetRepository(aggregateRoot.GetType()).CreateInstance(type);

                    DeserializeEntity(deserialized, types, repositories, entity, aggregateRoot, (Entity)instance);

                    this.InvokeRelationshipResolver(parent, instance);
                }
            }
        }

        private void DeserializeReferenceProperties(List<Entity> deserialized, Type[] types, IRepositoryRegistry repositories, XmlNode node, AggregateRoot aggregateRoot, Entity parent)
        {
            foreach (XmlNode primitive in node.SelectNodes("Property[@Type='Reference']"))
            {
                string name = primitive.Attributes["Name"].Value;

                //
                //  ok, get the next element
                XmlNode type = primitive.ChildNodes[0];
                Entity child = null;
                if (type != null)
                {
                    switch (type.Name)
                    {
                        case "AggregateRoot":
                            //
                            //  ok, this is where we deserialize the entity
                            child = DeserializeAggregateRoot(deserialized, types, repositories, type);
                            break;
                        case "Entity":
                            string typeName = type.Attributes["Type"].Value;
                            var entityType = types.FirstOrDefault(t => t.FullName == typeName);
                            object instance = repositories.GetRepository(aggregateRoot.GetType()).CreateInstance(entityType);

                            DeserializeEntity(deserialized, types, repositories, type, aggregateRoot, (Entity)instance);
                            child = (Entity)instance;
                            break;
                        case "Reference":
                            string referenceType = type.Attributes["Type"].Value;
                            Guid referenceId = Guid.Parse(type.Attributes["Id"].Value);
                            child = deserialized.FirstOrDefault(e => e.Id == referenceId && e.GetType() == types.FirstOrDefault(t => t.FullName == referenceType));
                            break;
                    }

                    var propertyInfo = parent.GetType().GetProperty(name);
                    propertyInfo.SetValue(parent, child, null);
                }
            }
        }

        private void DeserializePrimitiveProperties(XmlNode node, object instance)
        {
            foreach (XmlNode primitive in node.SelectNodes("Property[@Type='Primitive']"))
            {
                //
                //  ok, get the name
                string name = primitive.Attributes["Name"].Value;
                string value = primitive.InnerText;

                if (value != null && !string.IsNullOrEmpty(value))
                {
                    //
                    //  set it up
                    var propertyInfo = instance.GetType().GetProperty(name);
                    if(propertyInfo != null && propertyInfo.PropertyType != typeof(Guid))
                        propertyInfo.SetValue(instance, Convert.ChangeType(value, propertyInfo.PropertyType), null);
                    else
                        propertyInfo.SetValue(instance, Guid.Parse(value), null);
                }
            }
        }

        private void DeserializeSerializedProperties(XmlNode node, object instance)
        {
            foreach (XmlNode primitive in node.SelectNodes("Property[@Type='Serialized']"))
            {
                //
                //  ok, get the name
                string name = primitive.Attributes["Name"].Value;
                string assembly = primitive.Attributes["Assembly"].Value;
                string value = primitive.InnerText;

                if (value != null && !string.IsNullOrEmpty(value))
                {
                    //
                    //  set it up
                    var propertyInfo = instance.GetType().GetProperty(name);
                    propertyInfo.SetValue(instance, value.Base64Deserialize(AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.FullName == assembly)), null);
                }
            }
        }

        

        private void DeserializeAggregateRoot(Type [] types, IRepositoryRegistry repositories, XmlReader reader, string name, Action<object, object> attach)
        {/*
            //
            //  ok, here we go
            //  attempt to create an instance
            var type = types.First(t => t.Name == name);
            if (type != null)
            {
                //
                //  ok, lets get the repository
                IRepository r = repositories.GetRepository(type);
                object o = r.CreateInstance(type);
                object child;

                //
                //  time to populate the thing
                string propertyName = null;
                while (reader.Read())
                {
                    if (reader.NodeType == XmlNodeType.Element)
                    {
                        propertyName = reader.Name;
                    }

                    if (propertyName != null)
                    {
                        //
                        //  find the property
                        var pi = o.GetType().GetProperties().FirstOrDefault(p => p.Name == propertyName);
                        if (pi != null)
                        {
                            if (pi.PropertyType == typeof(string) || pi.PropertyType.IsPrimitive || pi.PropertyType == typeof(Guid))
                            {
                                //  set the value here
                                if (reader.Read())
                                {
                                    //  set the value of the property here
                                    piSetValue(o, Convert.ChangeType(reader.Value, pi.PropertyType), null);
                                }
                                propertyName = null;
                            }
                            else if (TypeIsChildEntityCollection(pi.PropertyType))
                            {
                                //
                                //  case where the property is a child entity collection
                            }
                            else if (pi.CanRead && pi.CanWrite && !TypeIsEntity(pi.PropertyType))
                            {
                                //  case where the value is a serialized value
                                pi.SetValue(o, reader.Value.Base64Deserialize(types.First().Assembly), null);
                            }
                        }
                    }
                    else
                        propertyName = null;
                }
            }*/
        }

        private Dictionary<Type, List<Guid>> written = new Dictionary<Type, List<Guid>>();

        public bool HasBeenSerialized(Entity o)
        {
            if(written.ContainsKey(o.GetType()))
                return written[o.GetType()].Contains(o.Id);
            else
                return false;
        }

        public void RegisteredSerialized(Entity o)
        {
            if(!written.ContainsKey(o.GetType()))
                written[o.GetType()] = new List<Guid>();

            written[o.GetType()].Add(o.Id);
        }

        public string Serialize(Entity o)
        {
            using(StringWriter sw = new StringWriter())
            {
                using (XmlTextWriter xml = new XmlTextWriter(sw))
                {
                    xml.Indentation = 1;
                    this.Serialize(xml, o);
                    return sw.GetStringBuilder().ToString();
                }
            }
            
        }

        public void Serialize(XmlWriter writer, Entity o)
        {
            if (o != null)
            {
                var type = FindType(o.GetType());
                if (!this.HasBeenSerialized(o))
                {
                    if (o is AggregateRoot)
                        writer.WriteStartElement("AggregateRoot");
                    else
                        writer.WriteStartElement("Entity");

                    writer.WriteAttributeString("Type", type.FullName);

                    this.RegisteredSerialized(o);

                    foreach (var pi in type.GetProperties())
                    {
                        if (pi.PropertyType == typeof(string) || pi.PropertyType.IsPrimitive || pi.PropertyType == typeof(Guid))
                        {
                            var v = pi.GetValue(o, null);
                            if (v != null)
                            {
                                writer.WriteStartElement("Property");
                                writer.WriteAttributeString("Type", "Primitive");
                                writer.WriteAttributeString("Name", pi.Name);
                                writer.WriteString(v.ToString());
                                writer.WriteEndElement();
                            }
                        }
                        else if (TypeIsChildEntityCollection(pi.PropertyType))
                        {
                            //
                            //  ok, this is a child entity collection
                            writer.WriteStartElement("Property");
                            writer.WriteAttributeString("Type", "ChildEntityCollection");
                            writer.WriteAttributeString("Name", pi.Name);

                            var v = pi.GetValue(o, null);
                            if (v != null && v is IEnumerable)
                            {
                                foreach (var cv in (IEnumerable)v)
                                {
                                    this.Serialize(writer, (Entity)cv);
                                }
                            }
                            writer.WriteEndElement();
                        }
                        else if (pi.CanRead && pi.CanWrite && !TypeIsEntity(pi.PropertyType))
                        {
                            var v = pi.GetValue(o, null);
                            if (v != null)
                            {
                                writer.WriteStartElement("Property");
                                writer.WriteAttributeString("Type", "Serialized");
                                writer.WriteAttributeString("Name", pi.Name);
                                writer.WriteAttributeString("Assembly", v.GetType().Assembly.FullName);
                                writer.WriteString( v.Base64Serialize());
                                writer.WriteEndElement();
                            }
                        }
                        else if (pi.CanRead && pi.CanWrite && TypeIsEntity(pi.PropertyType))
                        {
                            var v = pi.GetValue(o, null);
                            if (v != null)
                            {
                                writer.WriteStartElement("Property");
                                writer.WriteAttributeString("Type", "Reference");
                                writer.WriteAttributeString("Name", pi.Name);
                                //
                                //  this is virtual
                                //  so this is a case of just serialize it
                                this.Serialize(writer, (Entity)v);
                                writer.WriteEndElement();
                            }
                        }
                    }
                    writer.WriteEndElement();
                }
                else
                {
                    writer.WriteStartElement("Reference");
                    writer.WriteAttributeString("Type", type.FullName);
                    writer.WriteAttributeString("Id", o.Id.ToString());
                    writer.WriteEndElement();
                }
            }
        }

        private static Type FindType(Type type)
        {
            while (typeof(IPersistable).IsAssignableFrom(type))
            {
                type = type.BaseType;
            }

            return type;
        }

        private static bool TypeIsEntity(Type type)
        {
            while (type != null)
            {
                if (type == typeof(Entity))
                    return true;
                else
                    type = type.BaseType;
            }
            return false;
        }

        private static bool TypeIsChildEntityCollection(Type type)
        {
            while (type != null)
            {
                if (type.FullName.Contains("ChildEntityCollection"))
                    return true;
                type = type.BaseType;
            }

            return false;
        }

        public IEnumerable<Entity> Deserialize(Type [] types, RepositoryRegistry registry, string xml)
        {
            XmlDocument document = new XmlDocument();
            document.LoadXml(xml);

            return Deserialize(types, registry, document);
        }
    }
}
